package org.movee.net.cfg.parser.api.aspect;

import org.movee.net.cfg.parser.api.utils.ApiUtils;
import org.movee.net.cfg.parser.domain.api.ApiConstants;
import org.movee.net.cfg.parser.domain.api.ApiException;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;

/**
 *
 *
 * @author 
 */
@Slf4j
public class BasicApiAspect {

    @Getter
    private RateLimiter rateLimiter = null;

    public Object apiAroundAdvise(final ProceedingJoinPoint jp) throws Throwable {
        Instant start = Instant.now();
        Pair<String, String> httpContext = beforeProceedForHttp(jp, start);
        Object result = doProceed(jp);
        afterProceedForHttp(jp, httpContext.getLeft(), httpContext.getRight(), start);
        return result;
    }

    private Pair<String, String> beforeProceedForHttp(ProceedingJoinPoint jp, Instant start) {

        String requestId = "";
        String clientRealIp = "";

        RequestAttributes reqAttrs = RequestContextHolder.getRequestAttributes();
        // http context
        if (reqAttrs != null) {

            MethodSignature methodSignature = (MethodSignature) jp.getSignature();
            String targetName = methodSignature.getName();
            String[] pNames = methodSignature.getParameterNames();
            Object[] pValues = jp.getArgs();

            String params = formatMethodParameters(pNames, pValues);

            HttpServletRequest httpRequest = ((ServletRequestAttributes) reqAttrs).getRequest();
            clientRealIp = ApiUtils.getClientRealIp(httpRequest);

            if (httpRequest.getAttribute(ApiConstants.X_API_REQUEST_ID) == null) {
                // 本次请求第一次调用
                requestId = UUID.randomUUID().toString();
                httpRequest.setAttribute(ApiConstants.X_API_REQUEST_ID, requestId);

                httpRequest.setAttribute(ApiConstants.API_REQUEST_START, start);
                httpRequest.setAttribute(ApiConstants.API_REQUEST_ENTRY, targetName);

                MDC.put(ApiConstants.X_API_REQUEST_ID, requestId);

                Logger logger = LoggerFactory.getLogger(jp.getTarget().getClass());
                logger.info("clientRealIp: {}, start {}, parameters: {}", clientRealIp, targetName, params);
            }
        }

        return Pair.of(requestId, clientRealIp);
    }

    private Object doProceed(ProceedingJoinPoint jp) throws Throwable {
        Object result;
        if (rateLimiter == null) {
            result = jp.proceed();
        } else {
            // 开启限流
            result = RateLimiter.decorateSupplier(rateLimiter, () -> {
                try {
                    return jp.proceed();
                } catch (Throwable t) {
                    if (t instanceof ApiException) {
                        throw (ApiException) t;
                    } else if (t instanceof ConstraintViolationException) {
                        throw (ConstraintViolationException) t;
                    } else {
                        throw new RuntimeException("run into a api exception", t);
                    }
                }
            }).get();
        }
        return result;
    }

    private void afterProceedForHttp(ProceedingJoinPoint jp, String requestId, String clientRealIp, Instant start) {

        RequestAttributes reqAttrs = RequestContextHolder.getRequestAttributes();
        // http context
        if (reqAttrs != null) {
            HttpServletResponse httpResponse = ((ServletRequestAttributes) reqAttrs).getResponse();
            if (httpResponse != null) {
                httpResponse.setHeader(ApiConstants.X_API_REQUEST_ID, requestId);
                String date = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
                httpResponse.setHeader(ApiConstants.X_API_DATE, date);
                httpResponse.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

                // log info
                Duration duration = Duration.between(start, Instant.now());
                MethodSignature methodSignature = (MethodSignature) jp.getSignature();
                Logger logger = LoggerFactory.getLogger(jp.getTarget().getClass());
                logger.info("clientRealIp: {}, finish {}, duration time: {} ms",
                        clientRealIp, methodSignature.getName(), duration.toMillis());
            }
        }
    }

    private String formatMethodParameters(@NonNull final String[] pNames, @NonNull final Object[] pValues) {

        int len = Math.min(pNames.length, pValues.length);
        StringBuilder sb = new StringBuilder();
        int i = len;
        while (i > 1) {
            sb.append(String.format("%s=%s,", pNames[len - i], pValues[len - i]));
            i--;
        }

        if (i == 1) {
            sb.append(String.format("%s=%s", pNames[len - i], pValues[len - i]));
        } else {
            sb.append("none");
        }

        return sb.toString();

    }

    public void setRateLimiter(RateLimiterRegistry registry) {
        if (registry != null) {
            rateLimiter = registry.find("apiTotalLimit").orElse(null);
        }
    }

}
